</> 技術筆記Tech Notes

深入解析 JavaScript 的 `this`

深入解析 JavaScript 的 this

在 JavaScript 的世界裡,this 關鍵字可以說是最令人困惑、也最常被誤解的概念之一。它不像其他程式語言中的 thisself 那樣運作。要真正掌握 JavaScript,理解 this 的運作方式至關重要。

this 的核心概念

this 的值並非在函式定義時就固定,而是在函式被呼叫時才決定的。這意味著同一個函式,根據它被呼叫的方式不同,其內部的 this 可能會指向完全不同的物件。

幾個關鍵原則:

  • this 的值取決於函式如何被呼叫。

  • 一旦函式開始執行,其內部的 this 就無法被改變。

  • 可以透過 .call(), .apply(), 和 .bind() 方法來手動控制 this 的值。

this 的繫結規則

要判斷 this 到底指向誰,我們可以依循以下幾種常見的繫結規則。

1. 全域環境 (Global Context)

當函式在全域範疇下直接被呼叫時,this 會指向全域物件。在瀏覽器環境中,這個全域物件就是 window

var myFunction = function(){
    console.log(this); // 在瀏覽器中會印出 Window 物件
    console.log(this === window); // true
};

myFunction();

在嚴格模式 (Strict Mode) 下: 如果啟用嚴格模式 ('use strict'),在全域環境下呼叫函式,this 將會是 undefined,而不是 window 物件。這有助於避免意外污染全域物件。

'use strict'
var myFunction = function(){
    console.log(this); // undefined
    console.log(this === window); // false
};

myFunction();

巢狀函式呢? 即使是在一個函式內部再定義並呼叫另一個函式,只要內層函式是直接被呼叫,它的 this 仍然會遵循全域環境的規則。

function A() {
    function B() {
        // B() 是直接被呼叫,並非透過物件
        console.log(this === window); // true
    }
    B();
}
A();

2. 隱含的繫結 (Implicit Binding)

當函式作為一個物件的方法被呼叫時,this 會被設定為呼叫該方法的物件。這也是最常見的 this 用法。

var animal = {
    weight: 100,
    walk: function () {
        // walk() 是透過 animal 物件呼叫的
        console.log(this.weight); // 100
        console.log(this === animal); // true
    }
};

animal.walk();

失去隱含的繫結 這是一個常見的陷阱。如果我們將物件的方法指派給另一個變數,然後再透過這個新變數來呼叫函式,那麼「隱含的繫結」就會消失,this 會退回全域環境的規則。

function run() {
    console.log(this.model);
}

var car = {
    model: 'nissan',
    run: run
};

var alt = car.run; // 將方法指派給新變數 alt

alt(); // undefined (因為 this 指向 window,而 window.model 不存在)

在上面的例子中,alt() 執行時,它已經與 car 物件脫鉤,變成一個單純的全域函式呼叫。

3. 明確的繫結 (Explicit Binding)

有時候我們需要強制指定函式執行時的 this 是什麼,這時就可以使用 call()apply() 方法。

function run() {
    console.log(this);
}

var car = {
    maker: 'nissan',
    model: 'tiida'
};

// 使用 .call() 將 run 函式內的 this 強制指向 car 物件
run.call(car); // { maker: 'nissan', model: 'tiida' }

4. 硬繫結 (Hard Binding)

bind() 方法會建立一個新的函式,這個新函式的 this 會被永久地繫結到我們所指定的物件上,無論之後如何呼叫它。

function run() {
    console.log(this);
}

var car = {
    maker: 'nissan',
    model: 'tiida'
};

// 建立一個新函式 carRun,它的 this 永遠是 car
var carRun = run.bind(car);

carRun(); // { maker: 'nissan', model: 'tiida' }

5. new 繫結

當函式與 new 關鍵字一起使用時,這個函式就被當作一個建構子 (Constructor)。在這種情況下,this 會指向一個新建立的空物件

function Product(name) {
    // 當使用 new 時,this 會是一個新物件
    this.name = name;
    // 這個新物件會被隱含地回傳
}

var phone = new Product('Apple iPhone X');
console.log(phone.name); // 'Apple iPhone X'

6. IIFE (立即執行函式)

IIFE (Immediately Invoked Function Expression) 的 this 行為與一般函式相同,遵循全域環境的規則,即使它被定義在一個物件的方法內部。

var obj = {};
var someFunc = function () {
    console.log(this === obj); // true,因為 someFunc 是透過 obj 呼叫的

    // IIFE
    (function () {
        console.log("IIFE this:", this); // Window 物件
    })();
};

obj.func = someFunc;
obj.func();

如何判斷 this

總結來說,你可以依照以下順序來判斷 this 的值:

  1. 函式是否以 new 呼叫?

    • 若是,this 就是那個新建立的物件。

    • var bar = new foo();

  2. 函式是否以 callapply 呼叫?

    • 若是,this 就是明確指定的那個物件。

    • var bar = foo.call(obj);

  3. 函式是否作為物件的方法被呼叫 (隱含的繫結)?

    • 若是,this 就是那個物件。

    • var bar = obj.foo();

  4. 如果以上皆非

    • 這就是預設情況。在嚴格模式下,thisundefined;在一般模式下,this 是全域物件 (window)。